feat(merkle-tree-token-claimer): add pinocchio example - #714
Conversation
Greptile SummaryThe PR adds a Pinocchio implementation of the Merkle-tree token claimer, including initialization, root updates, proof-backed claims, receipt tracking, and LiteSVM coverage.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Reviews (2): Last reviewed commit: "merkle-tree-token-claimer: create PDAs o..." | Re-trigger Greptile |
Receipt and airdrop-state addresses are publicly derivable, and CreateAccount refuses to create over an account that already holds lamports. Anyone could therefore send a lamport to a receipt address and block that index's claim permanently. Top the account up, then Allocate and Assign — the same fallback Anchor's init performs.
|
@amilz could you take a look at this one when you get a chance? No open review threads left on it, so it is ready for maintainer review. It is one of 23 open Pinocchio ports I have up — they are independent and self-contained, so they can be reviewed and merged in any order: https://github.com/solana-developers/program-examples/pulls/MarkFeder |
|
Hey, in order to not overflow our reviewers with PRs, we'll limit the number of opened PRs to 3 per contributor for now. Thanks for the work! (You can re-open those whenever your other PRs get merged in) |
Adds a Pinocchio implementation of
merkle-tree-token-claimer, alongside the existing Anchor one.What it does
The airdrop pattern: fund a vault once, publish a Merkle root of the balance snapshot, and let each holder claim their allocation by proving membership.
Three instructions:
InitializeAirdropData— creates the mint, mints the whole supply into a vault owned by the airdrop PDA, records the root, then revokes the mint authority so the supply is fixed at what the vault holds.UpdateTree— replaces the root, allowed only before the first claim.ClaimAirdrop— verifies a proof, pays the claimant, and writes a receipt so the index cannot be claimed twice.Proof verification
The leaf is
claimer (32) | amount (8 LE), sha256-hashed; internal nodes aresha256(left || right);indexdoubles as the path, its low bit choosing the side at each level. Two properties are worth calling out because both are load-bearing and neither is obvious:indexmust be zero after the walk. The loop only consumes as many index bits as the proof has levels, so without the trailing check the same proof would also open receipt PDAs atindex + 2^depth,index + 2^(depth+1), … — one leaf, many payouts. Tested with an aliased index.The suite reuses the Anchor version's
tests/merkle.tsverbatim, so both implementations are checked against the same tree builder — including its zero-hash padding for odd levels (duplicating the last node instead would make a parentsha256(C || C), which verifies at two indices). The test tree has three leaves specifically to exercise that padding.Hashing uses
solana-sha256-hasher, which compiles to thesol_sha256syscall on SBF; it is added to the workspace dependencies here.Differences from the Anchor version
[amount, index, hashes…], with the proof length implied by the remaining bytes, rather than a BorshVec<u8>in the middle.init_if_neededcreates it first and then rolls back.One ordering note for reviewers: the already-claimed check runs before proof verification, so replaying a proof at an already claimed index reports
AlreadyClaimedrather thanInvalidProof. The test for proof binding therefore uses an unclaimed index.Tests
9 LiteSVM tests: the full lifecycle through to a drained vault, plus repeat-claim, stolen-proof, aliased-index, non-authority update and post-claim update rejections. Verified locally:
tsc --noEmit,pnpm test,prettier --check,cargo fmt --check,cargo clippy -D warnings.